/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.yinsin.jpabatis.mapper;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.yinsin.jpabatis.annotations.Param;
import com.yinsin.jpabatis.builder.BuilderException;
import com.yinsin.jpabatis.config.Configuration;
import com.yinsin.jpabatis.reflection.ParamNameUtil;

/**
 * @author Clinton Begin
 */
public class ResultMap {
	private Configuration configuration;

	private String id;
	private Class<?> type;
	private List<ResultMapping> resultMappings;
	private List<ResultMapping> idResultMappings;
	private List<ResultMapping> constructorResultMappings;
	private List<ResultMapping> propertyResultMappings;
	private Set<String> mappedColumns;
	private Set<String> mappedProperties;
	private Discriminator discriminator;
	private boolean hasNestedResultMaps;
	private boolean hasNestedQueries;
	private Boolean autoMapping;

	private ResultMap() {
	}

	public static class Builder {
		private static final Logger log = LoggerFactory.getLogger(Builder.class);

		private ResultMap resultMap = new ResultMap();

		public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
			this(configuration, id, type, resultMappings, null);
		}

		public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings, Boolean autoMapping) {
			resultMap.configuration = configuration;
			resultMap.id = id;
			resultMap.type = type;
			resultMap.resultMappings = resultMappings;
			resultMap.autoMapping = autoMapping;
		}

		public Builder discriminator(Discriminator discriminator) {
			resultMap.discriminator = discriminator;
			return this;
		}

		public Class<?> type() {
			return resultMap.type;
		}

		public ResultMap build() {
			if (resultMap.id == null) {
				throw new IllegalArgumentException("ResultMaps must have an id");
			}
			resultMap.mappedColumns = new HashSet<>();
			resultMap.mappedProperties = new HashSet<>();
			resultMap.idResultMappings = new ArrayList<>();
			resultMap.constructorResultMappings = new ArrayList<>();
			resultMap.propertyResultMappings = new ArrayList<>();
			final List<String> constructorArgNames = new ArrayList<>();
			for (ResultMapping resultMapping : resultMap.resultMappings) {
				resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
				resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
				final String column = resultMapping.getColumn();
				if (column != null) {
					resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
				} else if (resultMapping.isCompositeResult()) {
					for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
						final String compositeColumn = compositeResultMapping.getColumn();
						if (compositeColumn != null) {
							resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
						}
					}
				}
				final String property = resultMapping.getProperty();
				if (property != null) {
					resultMap.mappedProperties.add(property);
				}
				if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
					resultMap.constructorResultMappings.add(resultMapping);
					if (resultMapping.getProperty() != null) {
						constructorArgNames.add(resultMapping.getProperty());
					}
				} else {
					resultMap.propertyResultMappings.add(resultMapping);
				}
				if (resultMapping.getFlags().contains(ResultFlag.ID)) {
					resultMap.idResultMappings.add(resultMapping);
				}
			}
			if (resultMap.idResultMappings.isEmpty()) {
				resultMap.idResultMappings.addAll(resultMap.resultMappings);
			}
			if (!constructorArgNames.isEmpty()) {
				final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
				if (actualArgNames == null) {
					throw new BuilderException("Error in result map '" + resultMap.id + "'. Failed to find a constructor in '" + resultMap.getType().getName() + "' by arg names "
							+ constructorArgNames + ". There might be more info in debug log.");
				}
				Collections.sort(resultMap.constructorResultMappings, (o1, o2) -> {
					int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
					int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
					return paramIdx1 - paramIdx2;
				});
			}
			// lock down collections
			resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
			resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
			resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
			resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
			resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
			return resultMap;
		}

		private List<String> argNamesOfMatchingConstructor(List<String> constructorArgNames) {
			Constructor<?>[] constructors = resultMap.type.getDeclaredConstructors();
			for (Constructor<?> constructor : constructors) {
				Class<?>[] paramTypes = constructor.getParameterTypes();
				if (constructorArgNames.size() == paramTypes.length) {
					List<String> paramNames = getArgNames(constructor);
					if (constructorArgNames.containsAll(paramNames) && argTypesMatch(constructorArgNames, paramTypes, paramNames)) {
						return paramNames;
					}
				}
			}
			return null;
		}

		private boolean argTypesMatch(final List<String> constructorArgNames, Class<?>[] paramTypes, List<String> paramNames) {
			for (int i = 0; i < constructorArgNames.size(); i++) {
				Class<?> actualType = paramTypes[paramNames.indexOf(constructorArgNames.get(i))];
				Class<?> specifiedType = resultMap.constructorResultMappings.get(i).getJavaType();
				if (!actualType.equals(specifiedType)) {
					if (log.isDebugEnabled()) {
						log.debug("While building result map '" + resultMap.id + "', found a constructor with arg names " + constructorArgNames + ", but the type of '"
								+ constructorArgNames.get(i) + "' did not match. Specified: [" + specifiedType.getName() + "] Declared: [" + actualType.getName() + "]");
					}
					return false;
				}
			}
			return true;
		}

		private List<String> getArgNames(Constructor<?> constructor) {
			List<String> paramNames = new ArrayList<>();
			List<String> actualParamNames = null;
			final Annotation[][] paramAnnotations = constructor.getParameterAnnotations();
			int paramCount = paramAnnotations.length;
			for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
				String name = null;
				for (Annotation annotation : paramAnnotations[paramIndex]) {
					if (annotation instanceof Param) {
						name = ((Param) annotation).value();
						break;
					}
				}
				if (name == null && resultMap.configuration.isUseActualParamName()) {
					if (actualParamNames == null) {
						actualParamNames = ParamNameUtil.getParamNames(constructor);
					}
					if (actualParamNames.size() > paramIndex) {
						name = actualParamNames.get(paramIndex);
					}
				}
				paramNames.add(name != null ? name : "arg" + paramIndex);
			}
			return paramNames;
		}
	}
	
	public ResultMapping getResultMapping(String attrName){
		ResultMapping mapping = getResultMappingByColumn(attrName);
		if(null == mapping){
			mapping = getResultMappingByProperty(attrName);
		}
		return mapping;
	}
	
	public ResultMapping getResultMappingByProperty(String property){
		for(ResultMapping mapping : resultMappings){
			if(mapping.getProperty().equals(property)){
				return mapping;
			}
		}
		return null;
	}
	
	public ResultMapping getResultMappingByColumn(String column){
		for(ResultMapping mapping : resultMappings){
			if(column.equals(mapping.getColumn())){
				return mapping;
			}
		}
		return null;
	}

	public String getId() {
		return id;
	}

	public boolean hasNestedResultMaps() {
		return hasNestedResultMaps;
	}

	public boolean hasNestedQueries() {
		return hasNestedQueries;
	}

	public Class<?> getType() {
		return type;
	}

	public List<ResultMapping> getResultMappings() {
		return resultMappings;
	}

	public List<ResultMapping> getConstructorResultMappings() {
		return constructorResultMappings;
	}

	public List<ResultMapping> getPropertyResultMappings() {
		return propertyResultMappings;
	}

	public List<ResultMapping> getIdResultMappings() {
		return idResultMappings;
	}

	public Set<String> getMappedColumns() {
		return mappedColumns;
	}

	public Set<String> getMappedProperties() {
		return mappedProperties;
	}

	public Discriminator getDiscriminator() {
		return discriminator;
	}

	public void forceNestedResultMaps() {
		hasNestedResultMaps = true;
	}

	public Boolean getAutoMapping() {
		return autoMapping;
	}

}
